package sip

import (
	"bytes"
	"fmt"
	"strconv"
	"strings"
)

// ParseHostPort parse a text representation of a host[:port] pair.
// The port may or may not be present, so we represent it with a *uint16,
// and return 'nil' if no port was present.
func ParseHostPort(rawText string) (host string, port *Port, err error) {
	var rawHost, rawPort string
	if i := strings.LastIndex(rawText, ":"); i == -1 {
		rawHost = rawText
	} else {
		rawHost = rawText[:i]
		rawPort = rawText[i+1:]
	}

	if strings.HasPrefix(rawHost, "[") {
		// IPv6 with zone
		if zone := strings.Index(rawHost, "%25"); zone >= 0 {
			host1, er := Unescape(rawHost[:zone], EncodeHost)
			if er != nil {
				err = fmt.Errorf("unescape host: %w", er)
				return
			}
			host2, er := Unescape(rawHost[zone:len(rawHost)-1], EncodeZone)
			if er != nil {
				err = fmt.Errorf("unescape zone: %w", er)
				return
			}
			host3, er := Unescape(rawHost[len(rawHost)-1:], EncodeHost)
			if er != nil {
				err = fmt.Errorf("unescape host: %w", er)
				return
			}
			host = host1 + host2 + host3
		}
	}
	if host == "" {
		// IPv4 or IPv6 without zone
		if h, er := Unescape(rawHost, EncodeHost); er == nil {
			host = h
		} else {
			err = fmt.Errorf("unescape host: %w", er)
			return
		}
	}

	if rawPort != "" {
		// Surely there must be a better way..!
		var portRaw64 uint64
		var portRaw16 uint16
		portRaw64, err = strconv.ParseUint(rawPort, 10, 16)
		if err != nil {
			err = fmt.Errorf("parse port: %w", err)
			return
		}
		portRaw16 = uint16(portRaw64)
		port = (*Port)(&portRaw16)
	}

	return
}

// ParseParams is a general utility method for parsing 'key=value' parameters.
// Takes a string (source), ensures that it begins with the 'start' character provided,
// and then parses successive key/value pairs separated with 'sep',
// until either 'end' is reached or there are no characters remaining.
// A map of keys to values will be returned, along with the number of characters consumed.
// Provide 0 for start or end to indicate that there is no starting/ending delimiter.
// If quoteValues is true, values can be enclosed in double-quotes which will be validated by the
// parser and omitted from the returned map.
// If permitSingletons is true, keys with no values are permitted.
// These will result in a nil value in the returned map.
func ParseParams(
	source string,
	start uint8,
	sep uint8,
	end uint8,
	quoteValues bool,
	permitSingletons bool,
) (params Params, consumed int, err error) {
	params = NewParams()

	if len(source) == 0 {
		// Key-value section is completely empty; return defaults.
		return
	}

	// Ensure the starting character is correct.
	if start != 0 {
		if source[0] != start {
			err = fmt.Errorf(
				"expected %c at start of key-value section; got %c. section was %s",
				start,
				source[0],
				source,
			)
			return
		}
		consumed++
	}

	// Statefully parse the given string one character at a time.
	var buffer bytes.Buffer
	var key string
	parsingKey := true // false implies we are parsing a value
	inQuotes := false
parseLoop:
	for ; consumed < len(source); consumed++ {
		switch source[consumed] {
		case end:
			if inQuotes {
				// We read an end character, but since we're inside quotations we should
				// treat it as a literal part of the value.
				buffer.WriteString(string(end))
				continue
			}

			break parseLoop

		case sep:
			if inQuotes {
				// We read a separator character, but since we're inside quotations
				// we should treat it as a literal part of the value.
				buffer.WriteString(string(sep))
				continue
			}
			if parsingKey && permitSingletons {
				if k, er := Unescape(buffer.String(), EncodeQueryComponent); er == nil {
					params.Add(k, nil)
				} else {
					err = fmt.Errorf("unescape params: %w", er)
					return
				}
			} else if parsingKey {
				err = fmt.Errorf(
					"singleton param '%s' when parsing params which disallow singletons: \"%s\"",
					buffer.String(),
					source,
				)
				return
			} else {
				if k, er := Unescape(key, EncodeQueryComponent); er == nil {
					if v, er := Unescape(buffer.String(), EncodeQueryComponent); er == nil {
						params.Add(k, String{Str: v})
					} else {
						err = fmt.Errorf("unescape params: %w", er)
						return
					}
				} else {
					err = fmt.Errorf("unescape params: %w", er)
					return
				}
			}
			buffer.Reset()
			parsingKey = true

		case '"':
			if !quoteValues {
				// We hit a quote character, but since quoting is turned off we treat it as a literal.
				buffer.WriteString("\"")
				continue
			}

			if parsingKey {
				// Quotes are never allowed in keys.
				err = fmt.Errorf("unexpected '\"' in parameter key in params \"%s\"", source)
				return
			}

			if !inQuotes && buffer.Len() != 0 {
				// We hit an initial quote midway through a value; that's not allowed.
				err = fmt.Errorf("unexpected '\"' in params \"%s\"", source)
				return
			}

			if inQuotes &&
				consumed != len(source)-1 &&
				source[consumed+1] != sep {
				// We hit an end-quote midway through a value; that's not allowed.
				err = fmt.Errorf("unexpected character %c after quoted param in \"%s\"",
					source[consumed+1], source)

				return
			}

			inQuotes = !inQuotes

		case '=':
			if buffer.Len() == 0 {
				err = fmt.Errorf("key of length 0 in params \"%s\"", source)
				return
			}
			if !parsingKey {
				err = fmt.Errorf("unexpected '=' char in value token: \"%s\"", source)
				return
			}
			key = buffer.String()
			buffer.Reset()
			parsingKey = false

		default:
			if !inQuotes && strings.Contains(abnfWs, string(source[consumed])) {
				// Skip unquoted whitespace.
				continue
			}

			buffer.WriteString(string(source[consumed]))
		}
	}

	// The param string has ended. Check that it ended in a valid place, and then store off the
	// contents of the buffer.
	if inQuotes {
		err = fmt.Errorf("unclosed quotes in parameter string: %s", source)
	} else if parsingKey && permitSingletons {
		if k, er := Unescape(buffer.String(), EncodeQueryComponent); er == nil {
			params.Add(k, nil)
		} else {
			err = fmt.Errorf("unescape params: %w", er)
			return
		}
	} else if parsingKey {
		err = fmt.Errorf("singleton param '%s' when parsing params which disallow singletons: \"%s\"",
			buffer.String(), source)
	} else {
		if k, er := Unescape(key, EncodeQueryComponent); er == nil {
			if v, er := Unescape(buffer.String(), EncodeQueryComponent); er == nil {
				params.Add(k, String{Str: v})
			} else {
				err = fmt.Errorf("unescape params: %w", er)
				return
			}
		} else {
			err = fmt.Errorf("unescape params: %w", er)
			return
		}
	}
	return
}
